package fsm;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Stack;
import util.LOG;

public final class Board {

    private int m_width;
    private int m_height;
    /**
     * Das Farbfeld. Index einer Zelle ist X*Höhe+Y, dies vereinfacht das
     * Entfernen kompletter Spalten. Der Ursprung des Koordinatensystems ist
     * unten links.
     */
    private int[] m_data;
    // Enthält die die SpaltenIndizes der nicht-leeren Eintraege in der
    // obersten Zeile.
    // Wenn leer, kann die erste Zeile rausgeworfen werden.
    private int m_topRowCellCount;
    // Enthält die Anzahl der besetzten Felder.
    private int m_filledCells;
    //wird benoetigt, wenn ein removeGroup-Vorgang am Laufen ist, damit
    //mittendrin nicht ploetzlich Zeilen oder Spalten geloescht werden
    private boolean lockedField;
    /**
     * Kennzeichnet eine leere Zelle.
     */
    public static final int EMPTY = -1;
    public static final int maxX = 800;
    public static final int maxY = 600;

    public Board(int width, int height) {
        if (width > 0 && width <= maxX && height > 0 && height <= maxY) {
            lockedField=false;
            m_width = width;
            m_height = height;
            m_data = new int[width * height];
            m_topRowCellCount = width;
            m_filledCells = width * height;
        } else {
            throw new IllegalArgumentException("size beyond the maximum (" + maxX + "x" + maxY + ")");
        }
    }

    public Board(final Board other) {
        lockedField=other.lockedField;
        m_width = other.m_width;
        m_height = other.m_height;
        m_data = Arrays.copyOf(other.m_data, other.m_data.length);
        m_topRowCellCount = other.m_topRowCellCount;
        m_filledCells = other.m_filledCells;
    }

    //<editor-fold defaultstate="collapsed" desc="Getter, Setter">
    public final int getField(int x, int y) {
        return m_data[x * m_height + y];
    }

    public final int getField(int index){
        return m_data[index];
    }

    public final void setField(int x, int y, int value) {
        m_data[x * m_height + y] = value;
    }
    /*
     * direkten Index des felds ansprechen
     */

    public void setField(int index, int value) {
        m_data[index] = value;
    }

    public final void clearField(int x, int y) {
        m_data[x * m_height + y] = EMPTY;
    }

    public final void clearField(int index) {
        m_data[index] = EMPTY;
    }

    public final int getHeight() {
        return m_height;
    }

    public final int getWidth() {
        return m_width;
    }

    public int getTopRowCellCount() {
        return m_topRowCellCount;
    }

    //</editor-fold>

    //Soviel Zeit muss sein:
    private int getX(int index){
        return index/m_height;
    }
    private int getY(int index){
        return index%m_height;
    }

    /**
     * Entfernt eine komplette Spalte aus dem Feld und verkleinert das Feld
     * entsprechend.
     *
     * @param column Spaltenindex (0-basierend)
     */
    private void removeColumn(int column) {
        LOG.info("Remove column "+column+"...");
        assert m_width > 0;
        // Das neue Array hat eine Spalte weniger
        int[] temp = new int[m_height * (m_width - 1)];
        // Die ersten Spalten ohne Spalte "column" kopieren
        System.arraycopy(m_data, 0, temp, 0, m_height * column);
        // ... und nun den Rest dranhängen
        System.arraycopy(m_data, m_height * (column + 1), temp, m_height * column, (m_width - column - 1) * m_height);
        m_data = temp;
        m_width--;
    }

    private void removeTopRow() {
        LOG.info("Removing top Row...");
        int[] temp = new int[(m_height - 1) * m_width];
        // Wenn die TopRow nicht leer ist, ist irgendwo ein Fehler...
        assert m_topRowCellCount==0;
        m_topRowCellCount = 0;
        for (int x = 0; x < m_width; x++) {
            // Spalte ohne Top-Zelle kopieren
            System.arraycopy(m_data, x * m_height, temp, x * (m_height - 1), m_height - 1);
            // prüfen, ob die neue Top-Zelle leer ist
            if (temp[(x+1)*(m_height-1) - 1] != EMPTY) {
                m_topRowCellCount++;
            }
        }
        m_height--;
        m_data = temp;
    }

    public void removeCell(int index){
        removeCell(getX(index),getY(index));
    }

    public void removeCell(int x, int y) {
        /*
         * Alle Zellen mit Y-Koordinate > y rutschen um 1 nach unten, die
         * oberste Zelle wird EMPTY.
         */
        System.arraycopy(m_data, m_height * x + y + 1, m_data, m_height * x + y, m_height - y - 1);
        if( getField(x, m_height-1) != EMPTY ) {
            m_topRowCellCount--;
        }
        clearField(x, m_height - 1);
        //die topzeile darf nur am ende einer gruppen-remove-aktion entfernt werden
        if(!lockedField) {
            while(m_topRowCellCount==0) {
                removeTopRow();
            }
        }
        if (m_data[x * m_height] == EMPTY) {
            // Wenn das unterste Feld einer Spalte EMPTY ist, ist die Spalte leer :)
            removeColumn(x);
        }

        m_filledCells--;
        assert m_filledCells >= 0;
    }

    /**
     * Sucht eine Gruppe anhand des Floodfill-Algorithmus.
     *
     * @param index Index der Startzelle.
     * @return Hashset der gefundenen Zellen.
     */
    public HashSet<Integer> getGroup(int index) {
        HashSet<Integer> result = new HashSet<Integer>((m_width / 2) * (m_height / 2));
        Stack<Integer> todo = new Stack<Integer>();
        todo.add(index);
        int initColor = m_data[index];
        while (!todo.isEmpty()) {
            final int current = todo.pop();
            if (m_data[current] != initColor || result.contains(current)) {
                continue;
            }
            result.add(current);
            final int x = current / m_height;
            final int y = current % m_height;
            if (x + 1 < m_width) {
                todo.add(current + m_height);
            }
            if (x - 1 >= 0) {
                todo.add(current - m_height);
            }
            if (y + 1 < m_height) {
                todo.add(current + 1);
            }
            if (y - 1 >= 0) {
                todo.add(current - 1);
            }
        }
        return result;
    }


    /**
     * Alle Gruppen suchen
     *
     * @param onlyRemovable Wenn <tt>true</tt> werden nur Gruppen mit mindestens
     * 2 Elementen zurückgegeben.
     * @return Liste aller Farbgruppen.
     */
    public ArrayList<HashSet<Integer>> getAllGroups(boolean onlyRemovable) {
        ArrayList<HashSet<Integer>> result = new ArrayList<HashSet<Integer>>();
        HashSet<Integer> done = new HashSet<Integer>(m_filledCells);
        int currentIndex = 0;
        while (currentIndex < m_data.length) {
            HashSet<Integer> current = getGroup(currentIndex);
            done.addAll(current);
            if (!onlyRemovable || onlyRemovable && current.size() >= 2) {
                result.add(current);
            }
            // Das nächste nicht farblose Feld suchen. Bricht entweder bei Feldende
            // oder beim ersten Feld mit Farbe ab.
            while (++currentIndex < m_data.length) {
                if (!done.contains(currentIndex) && m_data[currentIndex] != EMPTY) {
                    break;
                }
            }
        }
        //assert done.size() == m_filledCells;
        return result;
    }

    public void removeGroup(HashSet<Integer> group){
        lockedField=true;
        while(!group.isEmpty()){
            //beim letzten element muss die sperre aufgehoben werden
            //damit das feld verkleinert werden kann, wenn noetig
            if(group.size()==1)
                lockedField=false;
            int indexToRemove = getMaxValueInGroup(group);
            removeCell(indexToRemove);
            group.remove(indexToRemove);
        }
        lockedField=false;
    }

    public void removeBiggestGroup(ArrayList<HashSet<Integer>> groups){
        HashSet<Integer> result = getBiggestGroup(groups);
        if(!result.isEmpty())
            removeGroup(result);
    }

    public final HashSet<Integer> getBiggestGroup(ArrayList<HashSet<Integer>> groups){
        HashSet<Integer> result = new HashSet<Integer>();
        for(HashSet<Integer> actualGroup : groups){
            if(actualGroup.size()>result.size())
                result=actualGroup;
        }
        return result;
    }

    public static int calculatePoints(int groupSize){
        return groupSize;
    }
    
    private int getMaxValueInGroup(HashSet<Integer> group){
        int result = -1;
        for(int i : group){
            if(i>result)
                result=i;
        }
        return result;
    }

    //<editor-fold defaultstate="collapsed" desc="toString()">
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        for(int i=m_height-1;i>=0;i--){
            for(int j=0;j<m_width;j++){
                sb.append((this.getField(j, i)==EMPTY)?"-":this.getField(j, i)).append(",");
            }
            sb.append("\n");
        }
        return sb.toString();
    }
    public String toString(HashSet<Integer> markedItems){
        StringBuilder sb = new StringBuilder();
        for(int i=m_height-1;i>=0;i--){
            for(int j=0;j<m_width;j++){
                String appender;
                int field = this.getField(j, i);
                if(markedItems.contains(j * m_height + i))
                    appender="("+((field==EMPTY)?"-":field)+")";
                else
                    appender=" "+((field==EMPTY)?"-":field)+" ";
                sb.append(appender);
            }
            sb.append("\n");
        }
        return sb.toString();
    }
    //</editor-fold>
}
